package cz.cvut.fel.parser;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Properties;

import javax.mail.BodyPart;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.Session;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimePart;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import cz.cvut.fel.email.Email;
import cz.cvut.fel.email.Email.EmailType;
import cz.cvut.fel.email.MIMEStatus;
import cz.cvut.fel.parser.PartNode.TYPE;

public class JavaMailParser implements Parser {

	public static final Logger log = LoggerFactory
			.getLogger(JavaMailParser.class);
	 
	private MessageDigest md;
	
	public JavaMailParser() {
		super();
		
		try {
			md = MessageDigest.getInstance("SHA1");
		} catch (NoSuchAlgorithmException e) {
			log.error("No digest available", e);

		}
	}

	protected static final byte[] Hexhars = {

		'0', '1', '2', '3', '4', '5',
		'6', '7', '8', '9', 'a', 'b',
		'c', 'd', 'e', 'f' 
		};
	
	private Email email;

	public static enum State {
		STARTING, INSECTION, WAITFORDATA, EXTRACTING, IGNORING, END
	};

	private void parseTree(BodyPart mp, PartNode current) throws IOException,
			MessagingException {
		Object content = null; 
	
		try {
			content = mp.getContent();
		} catch (IOException e) {
			 if (mp.getContentType().contains("charset=unicode-1-1-utf-7"))
			 {
				 ByteArrayOutputStream bao = new ByteArrayOutputStream();
				 mp.writeTo(bao);
				 content = bao.toString();
				 log.trace("Successful reset of unicode-1-1-utf-7");
			} else
				throw e;
		}

		if (content instanceof String) { // NOT MIME
			email.setType(EmailType.STRING);
			return;

		} else if (content instanceof Multipart) { // MIME
			current.setType(TYPE.MULTIPART);
			Multipart m = (Multipart) content;
			if (email.getRootNode() == null) { // Parsing first part
				current = new PartNode(null);
				// TODO: set headers
			}
			current.setContentType(m.getContentType());

			for (int i = 0; i < m.getCount(); i++) {
				BodyPart bp = m.getBodyPart(i);

				PartNode pn = new PartNode(current);
				pn.setContentType(bp.getContentType());
				current.addPart(pn);

				parseTree(bp, pn);
			}
		} else if (content instanceof InputStream) {
			// current.setContentType(content.)
		}

	}

	private void parseTree(Object content, PartNode current) throws Exception {

		if (content instanceof MimeMessage) {
			MimeMessage msg = (MimeMessage) content;

			for (int i = 0; i < HEADERS.length; i++) {
				String[] header = msg.getHeader(HEADERS[i]);
				if (header != null && header.length > 0) {
					email.addHeader(HEADERS[i], header[0]);
				}
			}

			content = ((MimeMessage) content).getContent();
		} else
			throw new Exception("Unknown type");

		if (content instanceof String) { // NOT MIME
			email.setType(EmailType.STRING);
			return;

		} else if (content instanceof Multipart) { // MIME
			Multipart m = (Multipart) content;
			if (email.getRootNode() == null) { // Parsing first part
				current = new PartNode(null);
				// TODO: set headers
			}
			current.setContentType(m.getContentType());

			for (int i = 0; i < m.getCount(); i++) {
				BodyPart bp = m.getBodyPart(i);
				PartNode pn = new PartNode(current);
				pn.setContentType(bp.getContentType());
				current.addPart(pn);

				parseTree(bp, pn);
			}
		} else {
			log.warn("Strange type: " + content.toString());
			email.setType(EmailType.STRING);
		}

	}

	public String byteToHex(byte[] b) {
	StringBuilder s = new StringBuilder(2 * b.length);

	for (int i = 0; i < b.length; i++) {

	int v = b[i] & 0xff;

	s.append((char)Hexhars[v >> 4]);
	s.append((char)Hexhars[v & 0xf]);
	}

	return s.toString();
	}
	public String constructObject(StringBuilder sb) {
	//md.update(sb.toString().getBytes());
		//log.info("HASH SHA1:"+byteToHex(md.digest()));
		return sb.toString();
	}

	private MailBinaryStruct deduplicatedJSON(ArrayList<MIMEStatus> plan,
			InputStream is) throws Exception {
		log.debug("plan size="+plan.size());
		MailBinaryStruct results = new MailBinaryStruct();
		BufferedReader br = new BufferedReader(new InputStreamReader(is));
		StringBuilder sb = new StringBuilder(200000);
		String line = null;
		int position = 0;
		MIMEStatus currentPlan = (plan.size() > 0 ? plan.get(0) : null);
		String currBoundary = (plan.size() > 0 ? plan.get(0).getBoundary()
				: null);
		String nextBoundary = (plan.size() > 1 ? plan.get(1).getBoundary()
				: null);
		State state = State.STARTING;
		StringBuilder extracted = new StringBuilder(100000);

		try {

			while ((line = br.readLine()) != null) {
				if (plan != null) {
					boolean foundBoundary = false;
					boolean foundNextBoundary = false;

					if (currBoundary != null && (line.startsWith(currBoundary)
							|| line.startsWith("--" + currBoundary)))
						foundBoundary = true;
					if (nextBoundary != null
							&& (line.startsWith(nextBoundary) || line
									.startsWith("--" + nextBoundary)))
						foundNextBoundary = true;

					switch (state) {
					case STARTING:
						if (foundBoundary) {
							state = State.INSECTION;
							if (currentPlan != null) {
								if (currentPlan.isExtract())
									state = State.WAITFORDATA;
								else
									state = State.IGNORING;
							}
						}
						sb.append(line + "\n");
						break;
					case IGNORING:
						if (foundNextBoundary) {
							if (position < plan.size() - 2) // Is next?
							{
								currentPlan = plan.get(++position);
								currBoundary = currentPlan.getBoundary();
								nextBoundary = plan.get(position + 1)
										.getBoundary();

								if (currentPlan.isExtract())
									state = State.WAITFORDATA;

							} else if (position == plan.size() - 2) { // LAST 2
								currentPlan = plan.get(++position);
								currBoundary = currentPlan.getBoundary();
								nextBoundary = currBoundary;

								if (currentPlan.isExtract())
									state = State.WAITFORDATA;

							} else if (position == plan.size() - 1) { // LAST1
								state = State.IGNORING;
							} else if (position == plan.size()) // EOF, should
																// not happen
								throw new Exception("Parsing error");
							else
								throw new Exception("Parsing error2");

						}

						sb.append(line + "\n");
						break;
					case INSECTION:
						throw new Exception("SHOULD NOT HAPPEN");
					case WAITFORDATA:
						if (line.equals("")) {
							state = State.EXTRACTING;
							sb.append("[-->ATTACHMENT #" + position + " <--]\n");
						}
						sb.append(line + "\n");
						break;
					case EXTRACTING:
						if (foundNextBoundary) {
							if (position < plan.size() - 2) // Is next?
							{
								currentPlan = plan.get(++position);
								currBoundary = currentPlan.getBoundary();
								nextBoundary = plan.get(position + 1)
										.getBoundary();

								if (currentPlan.isExtract())
									state = State.WAITFORDATA;

							} else if (position == plan.size() - 2) { // LAST
								currentPlan = plan.get(++position);
								currBoundary = currentPlan.getBoundary();
								nextBoundary = currBoundary;

								if (currentPlan.isExtract())
									state = State.WAITFORDATA;

							} else if (position == plan.size() - 1) { // LAST1
								state = State.IGNORING;

							} else if (position == plan.size()) // EOF, should
																// not happen
								throw new Exception("Parsing error");

							else
								throw new Exception("Parsing error2");

						}
						if (foundNextBoundary) {
							results.getAttachments().add(constructObject(extracted));
							extracted = new StringBuilder(100000);
							sb.append(line + "\n");
							// state = State.IGNORING;
							continue;
						} else if (!foundBoundary && !foundNextBoundary) {
							extracted.append(line + "\n");
							continue;
						} else {
							//results.add(constructObject(extracted));
							//extracted = new StringBuilder(100000);
							sb.append(line + "\n");
							continue;
						}
						
					}

					/*
					 * if (line.startsWith(currBoundary) ||
					 * line.startsWith("--"+currBoundary)) {
					 * 
					 * }
					 */
					// if (line.startsWith(nextBoundary)) ||
					// line.startsWith(prefix)
				}
			}

		} catch (IOException e) {
			log.error("IO PROBLEM",e);
		}
		results.setRAW(constructObject(sb));
		return results;
	}

	public void storeTmpResult(ArrayList<Object> results) throws IOException {
		for (int i = 0; i < results.size(); i++) {
			BufferedWriter writer = new BufferedWriter(
					new FileWriter("out" + i));
			if (i == results.size()-1)
				log.info(results.get(i).toString());
			writer.append(results.get(i).toString());
			writer.flush();
			writer.close();
		}
	}

	public Email parseEmail(byte[] data) {
		
		Session s = Session.getDefaultInstance(new Properties());
		email = new Email();

		InputStream cis;
		MimePart currentPart = null;

		try {
			//cis = new FileInputStream(new File(path));
			email.setIs(new ByteArrayInputStream(data));

			// 2. Get binary attachments
			currentPart = new MimeMessage(s, email.getIs());
			email.setRootNode(new PartNode(null));

			log.debug("Start parsing");
			parseTree(currentPart, email.getRootNode());
			log.debug("Stop parsing");

			ArrayList<MIMEStatus> plan = new ArrayList<MIMEStatus>();
			log.debug("Plan: ", email.getRootNode());
			boolean duplicate = email.getRootNode().fillDeduplicationPlan(plan);
			// log.info("Duplication: " + dupl + " plan: "+plan.toString());

			if (duplicate) {
				cis = new ByteArrayInputStream(data);
				MailBinaryStruct obs = deduplicatedJSON(plan, cis);

				//log.debug("SUM(RAW+DEDUPL)=" + obs.size());
				// log.info("DATA="+obs.get(obs.size()-1));
				//log.info(plan.toString());

				// log.info(obs.toString());
				//log.debug("SUM(RAW+DEDUPL)=" + obs.size());

				
				email.setAttachments(obs.getAttachments());
				email.setContent(obs.getRAW());
				email.setType(EmailType.MULTIPART);
			//	storeTmpResult(obs);
			} else {
				//log.info("IGNORE DEDUPLICATION");
				email.setType(EmailType.STRING);
				email.setContent(new String(data));
				
			}
		} catch (Exception e) {
			//log.error("General exception, try to store without deduplication",e);
			try {
				log.info("With Error:"+e.getMessage()+"\n Stack: ");
				e.printStackTrace();
				email.getAttachments().clear();
				email.getKeys().clear();
				email.setType(EmailType.STRING);
				email.setContent(new String(data));
			} catch (Exception e1) {
				log.error("Root", e);
				log.error("Failed to retry", e1);
			}
			log.info("Succeeded reseting");
		}
		
		for (int i = 0; i < email.getAttachments().size(); i++) {
			md.update(email.getAttachments().get(i).getBytes());					
			email.getKeys().add(byteToHex(md.digest()));
//			log.info("HASH SHA1:"+);
		}

		return email;
	}

}